/*
 * Copyright 2012 XueSong Guo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.webwheel.results;

import cn.webwheel.ResultInterpreter;
import cn.webwheel.WebContext;
import cn.webwheel.template.FileVisitor;
import cn.webwheel.template.RendererFactory;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Result interpreter for template result
 * @see TemplateResult
 */
public class TemplateResultInterpreter implements ResultInterpreter<TemplateResult> {

    protected Map<String, RendererInfo> rendererInfoMap = new ConcurrentHashMap<String, RendererInfo>();

    protected int cacheTTL = 5000;

    protected RendererFactory factory;

    protected String charset;

    protected String contentType;

    protected Map<Class, BeanGetter> getterMap = new ConcurrentHashMap<Class, BeanGetter>();

    public TemplateResultInterpreter(File root, String charset) {
        this.charset = charset;
        factory = new RendererFactory(root, charset, new ObjectMapper());
    }

    public int getCacheTTL() {
        return cacheTTL;
    }

    public void setCacheTTL(int cacheTTL) {
        this.cacheTTL = cacheTTL;
    }

    public String getCharset() {
        return charset;
    }

    public void setCharset(String charset) {
        this.charset = charset;
    }

    public String getContentType() {
        return contentType;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    @Override
    public void interpret(TemplateResult result, WebContext ctx) throws IOException, ServletException {

        String path = result.getPath();
        if (path == null) {
            path = ctx.getPath();
        }

        long now = System.currentTimeMillis();

        RendererInfo ri = rendererInfoMap.get(path);
        if (ri != null) {
            if (now - ri.lastCheckTime > cacheTTL) {
                long lm = 0;
                for (File file : ri.files) {
                    lm += file.lastModified();
                }
                if (lm != ri.lastModified) {
                    ri = null;
                }
            }
        }
        if (ri == null) {
            ri = new RendererInfo();
            final RendererInfo finalRi = ri;
            FileVisitor fv = new FileVisitor() {
                @Override
                public String read(File root, String file, String charset) throws IOException {
                    finalRi.files.add(new File(root, file));
                    return super.read(root, file, charset);
                }
            };
            ri.renderer = factory.create(path, fv, result.getTemplateCharset());
            for (File file : ri.files) {
                ri.lastModified += file.lastModified();
            }
            ri.lastCheckTime = now;
            rendererInfoMap.put(path, ri);
        }

        {
            String contentType = result.getContentType();
            if (contentType == null) contentType = this.contentType;
            if (contentType == null) contentType = ri.renderer.contentType;
            if (contentType == null) contentType = "text/html";
            ctx.getResponse().setContentType(contentType);
        }
        {
            String charset = result.getRenderCharset();
            if (charset == null) charset = this.charset;
            if (charset == null) charset = ri.renderer.charset;
            if (charset == null) charset = "utf-8";
            ctx.getResponse().setCharacterEncoding(charset);
        }

        BeanGetter beanGetter = getterMap.get(result.getCom().getClass());
        if (beanGetter == null) {
            getterMap.put(result.getCom().getClass(), beanGetter = new BeanGetter(result.getCom().getClass()));
        }
        Map<String, Object> map;
        try {
            map = beanGetter.set(result.getCom());
        } catch (Exception e) {
            throw new ServletException(e);
        }
        ri.renderer.render(ctx.getResponse().getWriter(), map);
    }

    static class BeanGetter {

        Field[] fields;
        Map<String, Method> methods = new HashMap<String, Method>();

        BeanGetter(Class cls) {
            fields = cls.getFields();
            for (Method method : cls.getMethods()) {
                Class<?> rt = method.getReturnType();
                if (rt == Void.class) continue;
                if (method.getParameterTypes().length > 0) continue;
                String name = method.getName();
                if (name.length() > 3 && name.startsWith("get")) {
                    methods.put(name.substring(3, 4).toLowerCase() + name.substring(4), method);
                } else if (name.length() > 2 && name.startsWith("is") && (rt == boolean.class || rt == Boolean.class)) {
                    methods.put(name.substring(2, 3).toLowerCase() + name.substring(3), method);
                }
            }
        }

        Map<String, Object> set(Object com) throws IllegalAccessException, InvocationTargetException {
            Map<String, Object> map = new HashMap<String, Object>();
            for (Field field : fields) {
                map.put(field.getName(), field.get(com));
            }
            for (Map.Entry<String, Method> entry : methods.entrySet()) {
                map.put(entry.getKey(), entry.getValue().invoke(com));
            }
            return map;
        }
    }
}
